Skip to content

feat: Add capture buffer for unknown/error bus frames with web UI page#44

Merged
marklynch merged 1 commit into
marklynch:mainfrom
lawther:feat/buffer-unknowns
Jun 24, 2026
Merged

feat: Add capture buffer for unknown/error bus frames with web UI page#44
marklynch merged 1 commit into
marklynch:mainfrom
lawther:feat/buffer-unknowns

Conversation

@lawther

@lawther lawther commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

I added a little utility to cache unknown/bad messages so I didn't have to be capturing logs all the time.

Screenshot 2026-06-23 at 08 20 51

@marklynch

Copy link
Copy Markdown
Owner

Great idea, I'll have a look over and get it merged in.

@marklynch marklynch left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: capture buffer for unknown/error bus frames

Overall: Solid, well-contained feature. Good mutex discipline, OOM checks throughout the JSON handler, XSS-safe DOM construction (textContent, not innerHTML), a host-test stub so the suite still links, a CHANGELOG entry, and the HTTP_MAX_URI_HANDLERS bump (14→20) correctly covers the 3 new endpoints. The payload offset math (raw[10]raw[len-3]) matches the real frame layout in message_decoder.c. ✅

Suggestions (low severity — none blocking)

  1. unknown_buffer_record() is called while holding the pool-state mutex (message_decoder.c, the messages_unknown_total++ branch). Every other call site releases state_mutex first; here it runs an O(capacity × 64-byte memcmp) dedup scan inside the critical section. There's no deadlock (it takes its own separate mutex), but it needlessly extends the pool-state critical section — worth moving the call after xSemaphoreGive(ctx->state_mutex) for consistency with the other sites.

  2. Transient heap pressure in the JSON handler. unknown_msgs_json_handler does malloc(UNKNOWN_BUFFER_CAPACITY * sizeof(unknown_entry_t)) ≈ ~9 KB per request, on top of the ~9 KB static s_entries[]. It's guarded with an OOM check so it degrades gracefully, but you could avoid the copy entirely by streaming entries to the response under the lock (or reduce capacity). The new free-heap readout on the Status page is handy for keeping an eye on this.

  3. Dedup collapses distinct long frames. Dedup keys on raw_len == len && memcmp(first store_len bytes), so two frames longer than 64 bytes that share their first 64 bytes and length merge into one entry. Acceptable given the truncation design — just worth a one-line comment noting it.

  4. Minor cleanup in unknown_msgs_json_handler. stored_len and store_len compute the same min(raw_len, MAX_RAW_BYTES) cap twice and could be a single variable. Also the inline frame-layout comment lists [CMD=7][LEN=8]... but skips [HDR_CHK=9] before DATA=10 — the offset is correct, the comment is just incomplete vs. the canonical one in message_decoder.c.

Nice utility overall — the truncation handling and "first N bytes of M" display in the UI are a nice touch.


Generated by Claude Code

@marklynch

Copy link
Copy Markdown
Owner

@lawther I did a Claude review above as don't have my computer with me. The keys one for me is point 1 due to previous scars from mutex locking, and the comment on point 3.

If you could have a look at those I would appreciate it.

Great improvement.

@lawther lawther force-pushed the feat/buffer-unknowns branch from 9a745f4 to 8112350 Compare June 23, 2026 23:27
Captures frames that have no registered decoder (unknown) or that fail
checksum/framing validation (error) into a fixed-capacity ring buffer
with LFU eviction. Dedup is keyed on error status, length, and the first
UNKNOWN_BUFFER_MAX_RAW_BYTES (64) bytes — frames that only differ beyond
byte 64 collapse into one entry, which is a known limitation.

Exposes the buffer via two HTTP endpoints:
  GET  /unknown-msgs-json  — JSON dump used by the web UI
  POST /unknown-msgs-clear — clears the buffer

The web UI page at /unknown-msgs lists entries sorted by most-recently-
seen, showing src/dst/cmd, decoded device names, hit count, timestamps,
payload bytes, and the raw frame.

Implementation notes:
- unknown_buffer uses its own mutex; lock_for_read()/unlock_after_read()
  let the JSON handler iterate s_entries directly without a heap copy.
- Hex strings use a single 193-byte stack buffer (MAX_RAW_BYTES * 3 + 1)
  reused for payload and raw fields; cJSON copies on each Add call.
- unknown_buffer_record() is called after xSemaphoreGive(state_mutex) at
  all call sites to keep the pool-state critical section short.
@lawther lawther force-pushed the feat/buffer-unknowns branch from 8112350 to 5222737 Compare June 24, 2026 02:59
@lawther

lawther commented Jun 24, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review Mark, I've addressed all the points. ptal.

  1. unknown_buffer_record() is called while holding the pool-state mutex (message_decoder.c, the messages_unknown_total++ branch). Every other call site releases state_mutex first; here it runs an O(capacity × 64-byte memcmp) dedup scan inside the critical section. There's no deadlock (it takes its own separate mutex), but it needlessly extends the pool-state critical section — worth moving the call after xSemaphoreGive(ctx->state_mutex) for consistency with the other sites.

Good catch. Done, it's neater this way too.

  1. Transient heap pressure in the JSON handler. unknown_msgs_json_handler does malloc(UNKNOWN_BUFFER_CAPACITY * sizeof(unknown_entry_t)) ≈ ~9 KB per request, on top of the ~9 KB static s_entries[]. It's guarded with an OOM check so it degrades gracefully, but you could avoid the copy entirely by streaming entries to the response under the lock (or reduce capacity). The new free-heap readout on the Status page is handy for keeping an eye on this.

Fair point. I originally went for the 'hold lock for minimal time' approach, but now I've taken the 'stream entries under lock' approach. If the lock can't be acquired, it returns a HTTP 500 with a message to retry. Given the way that lock acquisition waits up to 100ms, things would have to be pretty overloaded for this to happen.

  1. Dedup collapses distinct long frames. Dedup keys on raw_len == len && memcmp(first store_len bytes), so two frames longer than 64 bytes that share their first 64 bytes and length merge into one entry. Acceptable given the truncation design — just worth a one-line comment noting it.

Comment added.

I did originally consider individually mallocing storage for each unknown frame as it came in to ensure proper storage and dedupe. I thought it was overly fraught, and any fragmentation from lots of small mallocs would probably eat into any savings. I'm not 100% sure how the allocator works. So I came down on the side of a static block, taking ~9KB, which I thought was a worthwhile tradeoff.

  1. Minor cleanup in unknown_msgs_json_handler. stored_len and store_len compute the same min(raw_len, MAX_RAW_BYTES) cap twice and could be a single variable. Also the inline frame-layout comment lists [CMD=7][LEN=8]... but skips [HDR_CHK=9] before DATA=10 — the offset is correct, the comment is just incomplete vs. the canonical one in message_decoder.c.

All done.

@marklynch marklynch merged commit 2becd63 into marklynch:main Jun 24, 2026
2 checks passed
@marklynch

Copy link
Copy Markdown
Owner

Thanks for fixing up. Merged in

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants